<?php
/**
 * super class of data models
 * NOTE: this is a pure data relational mapping
 * NO CRUD is built in, to maximize compatibility
 * save/update/delete should be done via DAOs
 *
 * @author bbqq <toughgamer@gmail.com>
 * @package com.blib.data
 * @copyright bbqq qstufie.com
 */
abstract class Blib_Data_Model_Base extends Blib_Class_Atomic implements Blib_Interface_Data_Persistable
{
/*--------------------------------------[properties]--------------------------------------*/

    /**
     * the unique id field(s)
     * must be set in the extended class
     *
     * @access protected
     * @var    mixed, can be one field, or array of fields (combined key)
     */
    protected $keys = null;


    /**
     * table name
     * must be set in the extended class
     *
     * @access protected
     */
    protected $tableName = null;


    /**
     * id value(s) of the table
     *
     * @access protected
     * @var    mixed - can be array or int or even string
     */
    protected $id = null;


    /**
     * the raw data array
     * used for caching, building up the object, etc.
     *
     * @access protected
     * @var    array
     */
    protected $rawData;


    /**
     * the external data KEY array
     * used for external objects/values attached to this object
     * so it doesn't ruin the rawData which will be used to save into db
     * IT STORES ONLY THE KEYS - SO WHEN USING GETRAWDATA, THESE ARE EXEMPTED
     * MUST BE SET BEFORE HAND
     *
     * @format array['name' => 'type']
     * @access protected
     * @var    array
     */
    protected $externalVars = array();


    /**
     * the internal data KEY array
     * used for internal objects/values attached to this object
     * MUST BE SET BEFORE HAND
     *
     * @format array['name' => 'type']
     * @access protected
     * @var    array
     */
    protected $internalVars = array();


    /**
     * is it a new object?
     * this relates to update/insert action
     *
     * @var bool
     */
    private $_isNew = false;


    /**
     * construct
     * always init with the id,
     * default as 0 for a new one
     *
     * @param array $rawData the rawData to build the object
     * @param bool  $isNew   is it a new object? or old? determines update or insert
     *
     * @return void
     */
    public function __construct($rawData = array(), $isNew = true)
    {
        // any overhead stuff can be done here...
        $this->init();

        // build up by raw data
        $this->setup($rawData, $isNew);

    }// end __construct


    /**
     * abstract function for init...
     * not defined as abstract so it can be omitted
     *
     * @return void
     */
    protected function init()
    {

    }// end init

/*--------------------------------------[getters]--------------------------------------*/

    /**
     * get the id of the object
     *
     * @param bool $flat if true, return a flat str id for special use
     *
     * @return mixed $id the id
     */
    public function getID($flat = false)
    {
        if ($flat && is_array($this->id)) {
            return join('_', $this->id);
        }

        // no preprocess
        return $this->id;

    }// end getID


    /**
     * get table name for db operation
     *
     * @return str
     */
    public function __getTableName()
    {
        return $this->tableName;

    }// end __getTableName


    /**
     * get the primary key field name
     *
     * @return mixed
     */
    public function __getUniqueIdName()
    {
        return $this->keys;

    }// end __getUniqueIdName


    /**
     * get raw data array
     *
     * @return array
     */
    public function __getRawData()
    {
        // now, kill the external data...
        $data = array_diff_key($this->rawData, $this->externalVars);

        // now... if it's not an auto incremental table... with combined keys...
        if (is_array($this->id)) {
            $data = array_merge($data, $this->id);
        }

        return $data;

    }// end __getRawData


    /**
     * get data vars out...
     *
     * @param str   $name    the name
     * @param mixed $default a default value
     *
     * @return mixed
     */
    protected function _getVar($name, $default = null)
    {
        // force lowercase
        $name = strtolower($name);
        $val = null;

        if (isset($this->rawData[$name])) {
            $val = $this->rawData[$name];
        }

        if (empty($val) && !empty($default)) {
           $val = $default;
        }

        // avoid null input...
        if (empty($val)) {
            $type = $this->_getType($name);
            // make default out
            switch ($type) {
                case 'int':
                case 'integer':
                    $val = 0;
                    break;

                case 'float':
                case 'decimal':
                    $val = 0.00;
                    break;

                case 'array':
                    $val = array();
                    break;

                default:
                    if ($this->_isClass($type)) {
                        $val = new $type;
                    }
            }
        }

        return $val;

    }// end _getVar


    /**
     * get type
     *
     * @param str $name name of val
     *
     * @return str
     */
    protected function _getType($name)
    {
        $type = false;
        if (isset($this->internalVars[$name])) {
            $type = $this->internalVars[$name];
        } elseif (isset($this->externalVars[$name])) {
            $type = $this->externalVars[$name];
        }

        return $type;

    }// end _getType


    /**
     * get data time by given format
     *
     * @param str $name    the name of the variable
     * @param str $format  format of date, such as jS, F Y...
     * @param str $default default value
     *
     * @return str
     */
    protected function _getDatetime($name, $format, $default = null)
    {
        $val = $this->_getVar($name);
        if (empty($val)) {
            return $default;
        }

        // if format is false, then return raw...
        if ($format === false) {
            return $val;
        }

        // otherwise, calculate it..
        return Blib_Utility_Date::formatTimeStamp($val, $format);

    }// end _getDatetime

/*--------------------------------------[utility]--------------------------------------*/

    /**
     * is it new?
     *
     * @return bool
     */
    public function isNew()
    {
        return $this->_isNew;

    }// end isNew


    /**
     * check if one type is a class
     *
     * @param str $type the type
     *
     * @return bool
     */
    private function _isClass($type)
    {
        // note: we are not using all types of objects...
        if (strpos($type, '_') !== false || class_exists($type)) {
             return true;
        }

        return false;

    }// end _isClass

/*--------------------------------------[setters]--------------------------------------*/

    /**
     * set the id
     *
     * @param mixed $id the id
     *
     * @return void
     */
    public function setID($id)
    {
        // need to work out if it's valid
        if (is_array($this->keys)) {
            // then id must be array too...
            if (!is_array($id) || count($id) != count($this->keys)) {
                throw new Exception_Model($this, $id,
                    'ID of object ' . __class__ . ' is not valid');
            }
        }

        $this->id = $id;
        return $this;

    }// end setID


    /**
     * set the id from raw data
     * @param array $rawData the raw data array
     * @return $this;
     */
    public function setIdFromRawData($rawData, $isNew = true)
    {
        $id = 0;
        if (is_array($rawData)) {
            // test if keys contains the stuff...
            if (is_array($this->keys)) {
                $id = array();
                foreach ($this->keys as $key) {
                    if (isset($rawData[$key])) {
                        $id[$key] = $rawData[$key];
                    }
                }
            } else {
                if (isset($rawData[$this->keys])) {
                    $id = $rawData[$this->keys];
                }
            }
        }

        // constructor...
        $this->id = $id;

        // set stats for both single id and composite...
        // if composite, join the ids...
        $tmpId = $id;
        if (is_array($id)) {
            $tmpId = join('', $id);
        }

        if (!empty($tmpId)) {
            $isNew = false;
        }

        $this->flagUpdate($isNew);

        return $this;

    }// end setIdFromRawData


    /**
     * set the status of object, new or existing
     *
     * @param bool $isNew true for update
     *
     * @return void
     */
    public function flagUpdate($isNew)
    {
        $this->_isNew = (bool) $isNew;
        return $this;

    }// end flagUpdate


    /**
     * set value according to the type...
     *
     * @param str   $name the name of value
     * @param mixed $val  the value
     *
     * @return void
     */
    protected function _setVar($name, $val)
    {
        // force lower case
        $name = strtolower($name);
        $type = $this->_getType($name);

        if (!$type) {
            // means the var doesn't exist
            return false;
        }
        // first, force type check...
        switch ($type) {
            case 'str':
            case 'string':
                $val = (string) $val;
                break;

            case 'int':
            case 'integer':
                $val = (int) $val;
                break;

            case 'float':
                $val = (float) $val;
                break;

            case 'decimal':
                $val = (float) $val;
                break;

            case 'array':
                $val = (array) $val;
                break;

            default:
                if ($this->_isClass($type)) {
                    // then we're expecting an object!!!
                    if (!$val instanceof $type) {
                        throw new Exception('Set value error in ' . __class__ .
                                '. Type of [' . $name . '] is expected to be [' .
                                $type . '].');
                    }
                }

        }

        $this->rawData[$name] = $val;
        return $this;   // always return self so chain set is available...

    }// end _setVar


    /**
     * add one object into an array
     *
     * @param Blib_Data_Model_Base $object the object
     * @param str                                   $name   the name of array val
     *
     * @return void
     */
    protected function _addOneObject(Blib_Data_Model_Base $object, $name)
    {
        // do $name add if not already...
        $vars = $this->_getVar($name, array());

        // add to it...
        $vars[$object->getID(true)] = $object;

        // set it back in
        $this->_setVar($name, $vars);

        // unset vars
        unset($vars);
        // enable chain...
        return $this;

    }// end _addOneObject


    /**
     * add one item into an array
     *
     * @param str $name  array name
     * @param str $value item value
     * @param str $key   item key in the array, if null, add a new one
     *
     * @return void
     */
    protected function _addOneItem($name, $value, $key = null)
    {
        // do $name add if not already...
        $vars = $this->_getVar($name, array());

        // add to it...
        $vars[$key] = $value;

        // set it back in
        $this->_setVar($name, $vars);

        // unset vars
        unset($vars);
        // enable chain...
        return $this;

    }// end _addOneItem

/*--------------------------------------[data operations]--------------------------------------*/

    /**
     * set up the object with rawData
     *
     * @param object $rawData
     * @param bool $isNew is it a new object?
     * @return self;
     */
    public function setup($rawData, $isNew = true)
    {
        // is there id?
        $this->setIdFromRawData($rawData, $isNew);

        // here, use self var setup...
        foreach ($rawData as $key => $val) {
            if (!empty($val)) {
                $method = 'set' . Blib_Utility_String::camelCase($key, '_');

                // check if there's this var in it...
                if (isset($this->internalVars[$key]) ||
                    isset($this->externalVars[$key])) {
                    // first... is there a customised setup func?
                    // to find out, get the name out...
                    if (method_exists($this, $method)) {
                        call_user_func_array(array($this, $method), array($val));
                    } else {
                        $this->_setVar($key, $val);
                    }

                }// if isset($this->internalVars[$key]) ||

            }// if !emtpy(val)

        }// foreach

        // enable chain...
        return $this;

    }// end setup

/*--------------------------------------[persistence]--------------------------------------*/

    // get PID
    public function ___getPID()
    {
        return $this->getID(true);

    }// end __getPID

/*--------------------------------------[flatten]--------------------------------------*/

    /**
     * write as xml
     * NOTE: only internal vars
     * @param bool $segmented if true, only write segments, default to true
     * @return str xml string
     */
    public function __toXml($segmented = true)
    {
        // body;
        $xml = '';

        // first of all, self id
        if (is_array($this->keys)) {
            foreach ($this->keys as $tag) {
                $value = $this->id[$tag];
                $xml  .= "<$tag>$value</$tag>";
            }
        } else {
            $tag    = $this->keys;
            $value  = $this->id;
            $xml   .= "<$tag>$value</$tag>";
        }

        foreach ($this->internalVars as $tag => $value) {
            $value = $this->_getVar($tag);
            if (is_string($value)) {
                $value = "<![CDATA[$value]]>";
            }
            $xml .= "<$tag>$value</$tag>";
        }

        $parentTag = substr($this->tableName, 0, strlen($this->tableName) - 1);
        $xml = "<$parentTag>$xml</$parentTag>";

        // if segmented
        if ($segmented) {
            return $xml;
        }

        // otherwise, return proper xml with xml tag
        return '<?xml version="1.0" encoding="UTF-8"?>' . $xml;

    }// end __toXml

}// end Blib_Data_Model_Base
?>